winbrew_core\fs\archive\extract/
tar.rs

1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Component, Path, PathBuf};
4
5use bzip2::read::BzDecoder;
6use flate2::read::GzDecoder;
7
8use crate::fs::{FsError, Result};
9
10use super::super::context::ExtractionContext;
11use super::super::limits::ExtractionLimits;
12use super::super::platform::PlatformAdapter;
13
14const TAR_COPY_BUFFER_SIZE: usize = 256 * 1024;
15
16pub(crate) fn extract_tar_archive_with_platform<P: PlatformAdapter>(
17    archive_path: &Path,
18    destination_dir: &Path,
19    limits: ExtractionLimits,
20) -> Result<()> {
21    let archive_file =
22        fs::File::open(archive_path).map_err(|err| FsError::open_archive(archive_path, err))?;
23    let archive_size = fs::metadata(archive_path)
24        .map_err(|err| FsError::open_archive(archive_path, err))?
25        .len();
26    let reader = archive_reader_for_path(archive_path, archive_file);
27    let mut archive = tar::Archive::new(reader);
28    let mut extraction = ExtractionContext::<P>::new(limits);
29    let mut buffer = vec![0u8; TAR_COPY_BUFFER_SIZE];
30
31    let entries = archive
32        .entries()
33        .map_err(|err| FsError::read_archive_entry(archive_path, err))?;
34
35    for entry in entries {
36        let mut entry = entry.map_err(|err| FsError::read_archive_entry(archive_path, err))?;
37        extract_entry(
38            &mut entry,
39            archive_size,
40            destination_dir,
41            &mut extraction,
42            &mut buffer,
43        )?;
44    }
45
46    extraction.commit();
47    Ok(())
48}
49
50fn archive_reader_for_path(archive_path: &Path, file: fs::File) -> Box<dyn Read> {
51    let file_name = archive_path
52        .file_name()
53        .and_then(|name| name.to_str())
54        .unwrap_or_default()
55        .to_ascii_lowercase();
56
57    if file_name.ends_with(".tar.gz") || file_name.ends_with(".tgz") {
58        Box::new(GzDecoder::new(file))
59    } else if file_name.ends_with(".tbz2") || file_name.ends_with(".tar.bz2") {
60        Box::new(BzDecoder::new(file))
61    } else {
62        Box::new(file)
63    }
64}
65
66fn extract_entry<P: PlatformAdapter, R: Read>(
67    entry: &mut tar::Entry<'_, R>,
68    archive_size: u64,
69    destination_dir: &Path,
70    extraction: &mut ExtractionContext<P>,
71    buffer: &mut [u8],
72) -> Result<()> {
73    let entry_path = entry
74        .path()
75        .map_err(|_| FsError::invalid_archive_entry_path())?;
76    let enclosed_name = sanitize_entry_path(entry_path.as_ref())?;
77
78    let outpath = destination_dir.join(&enclosed_name);
79
80    extraction.validate_target(&outpath, destination_dir)?;
81    extraction.check_limits(&enclosed_name, entry.size(), archive_size)?;
82
83    let entry_type = entry.header().entry_type();
84
85    if entry_type.is_symlink() {
86        return Err(FsError::symlink_entry(&outpath));
87    }
88
89    if entry_type.is_hard_link() {
90        return Err(FsError::unsupported_entry(&outpath));
91    }
92
93    if entry_type.is_dir() {
94        extraction.ensure_directory_tree(&outpath)?;
95        return Ok(());
96    }
97
98    if !entry_type.is_file() {
99        return Err(FsError::unsupported_entry(&outpath));
100    }
101
102    if let Some(parent) = outpath.parent() {
103        extraction.ensure_directory_tree(parent)?;
104    }
105
106    let mut outfile = P::create_extraction_target_file(&outpath)
107        .map_err(|err| FsError::create_extracted_file(&outpath, err))?;
108    extraction.record_file(&outpath);
109
110    loop {
111        let bytes_read = entry
112            .read(buffer)
113            .map_err(|err| FsError::read_entry(&outpath, err))?;
114        if bytes_read == 0 {
115            break;
116        }
117
118        outfile
119            .write_all(&buffer[..bytes_read])
120            .map_err(|err| FsError::write_entry(&outpath, err))?;
121    }
122
123    Ok(())
124}
125
126fn sanitize_entry_path(path: &Path) -> Result<PathBuf> {
127    let mut enclosed = PathBuf::new();
128
129    for component in path.components() {
130        match component {
131            Component::Normal(part) => enclosed.push(part),
132            Component::CurDir => {}
133            _ => return Err(FsError::invalid_archive_entry_path()),
134        }
135    }
136
137    if enclosed.as_os_str().is_empty() {
138        return Err(FsError::invalid_archive_entry_path());
139    }
140
141    Ok(enclosed)
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::fs::archive::{ArchiveKind, extract_archive};
147    use std::fs;
148    use tempfile::tempdir;
149
150    fn create_tar_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
151        let file = fs::File::create(path).expect("create tar file");
152        let mut builder = tar::Builder::new(file);
153        let mut header = tar::Header::new_gnu();
154        header.set_size(contents.len() as u64);
155        header.set_mode(0o644);
156        header.set_cksum();
157
158        builder
159            .append_data(&mut header, file_name, contents)
160            .expect("append tar entry");
161        builder.finish().expect("finish tar file");
162    }
163
164    fn create_tar_gz_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
165        let file = fs::File::create(path).expect("create tar.gz file");
166        let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
167        let mut builder = tar::Builder::new(encoder);
168        let mut header = tar::Header::new_gnu();
169        header.set_size(contents.len() as u64);
170        header.set_mode(0o644);
171        header.set_cksum();
172
173        builder
174            .append_data(&mut header, file_name, contents)
175            .expect("append tar.gz entry");
176        let encoder = builder.into_inner().expect("finish tar builder");
177        encoder.finish().expect("finish tar.gz file");
178    }
179
180    fn create_tar_bz2_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
181        let file = fs::File::create(path).expect("create tar.bz2 file");
182        let encoder = bzip2::write::BzEncoder::new(file, bzip2::Compression::default());
183        let mut builder = tar::Builder::new(encoder);
184        let mut header = tar::Header::new_gnu();
185        header.set_size(contents.len() as u64);
186        header.set_mode(0o644);
187        header.set_cksum();
188
189        builder
190            .append_data(&mut header, file_name, contents)
191            .expect("append tar.bz2 entry");
192        let encoder = builder.into_inner().expect("finish tar builder");
193        encoder.finish().expect("finish tar.bz2 file");
194    }
195
196    #[test]
197    fn extract_tar_archive_extracts_plain_tar() {
198        let temp_dir = tempdir().expect("temp dir");
199        let destination_dir = temp_dir.path().join("dest");
200        let archive_path = temp_dir.path().join("archive.tar");
201
202        fs::create_dir_all(&destination_dir).expect("destination dir");
203        create_tar_archive(&archive_path, "bin/tool.exe", b"tar payload");
204
205        extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir).expect("tar extraction");
206
207        assert_eq!(
208            fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
209            b"tar payload"
210        );
211    }
212
213    #[test]
214    fn extract_tar_archive_extracts_tar_gz() {
215        let temp_dir = tempdir().expect("temp dir");
216        let destination_dir = temp_dir.path().join("dest");
217        let archive_path = temp_dir.path().join("archive.tar.gz");
218
219        fs::create_dir_all(&destination_dir).expect("destination dir");
220        create_tar_gz_archive(&archive_path, "bin/tool.exe", b"tar gz payload");
221
222        extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir)
223            .expect("tar.gz extraction");
224
225        assert_eq!(
226            fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
227            b"tar gz payload"
228        );
229    }
230
231    #[test]
232    fn extract_tar_archive_extracts_tar_bz2() {
233        let temp_dir = tempdir().expect("temp dir");
234        let destination_dir = temp_dir.path().join("dest");
235        let archive_path = temp_dir.path().join("archive.tar.bz2");
236
237        fs::create_dir_all(&destination_dir).expect("destination dir");
238        create_tar_bz2_archive(&archive_path, "bin/tool.exe", b"tar bz2 payload");
239
240        extract_archive(ArchiveKind::Tar, &archive_path, &destination_dir)
241            .expect("tar.bz2 extraction");
242
243        assert_eq!(
244            fs::read(destination_dir.join("bin/tool.exe")).expect("read"),
245            b"tar bz2 payload"
246        );
247    }
248}